package Tracer;

import java.awt.Point;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

/**
 * ClassName : RTBitmap. This is a class for storing raw data of bitmaps. It
 * provides operations to construct a bitmap from PPM file and also to write the
 * bitmap to a PPM file.
 * 
 * @author forrest lorraine eagle
 * @version 1.0
 */
public class RTBitmap {
	/**
	 * the width of bitmap
	 */
	private int bWidth;
	/**
	 * the height of bitmap
	 */
	private int bHeight;
	/**
	 * the pixel color of bitmap
	 */
	private RTColor[][] bColorBitmap;

	/**
	 * Default constructor of the class, simply set all member to 0/null
	 */
	public RTBitmap() {
		bWidth = 0;
		bHeight = 0;
	}

	/**
	 * Constructor to initialize a bitmap object in size of (width,height)
	 * 
	 * @param width
	 * @param height
	 */
	public RTBitmap(final int width, final int height) {
		bWidth = width;
		bHeight = height;
		bColorBitmap = new RTColor[bHeight][bWidth];
		for (int i = 0; i < bHeight; i++) {
			for (int j = 0; j < bWidth; j++) {
				bColorBitmap[i][j] = new RTColor(0.0f, 0.0f, 0.0f);
			}
		}
	}

	/**
	 * Set color of a specific point.
	 * 
	 * @param pt
	 *            The point in the bitmap.
	 * @param color
	 *            The color to set.
	 */
	public final void setColorAtPoint(final Point pt, final RTColor color) {
		if (pt.x >= bWidth || pt.x < 0 || pt.y < 0 || pt.y >= bHeight) {
			throw new IllegalArgumentException("Invalid point coordinate");
		}
		bColorBitmap[pt.y][pt.x].setColor(color);
	}

	/**
	 * Get the color of a specific point in the bitmap, if bitmap is null,
	 * return null.
	 * 
	 * @param pt
	 *            The coordinate of the point to get.
	 * @return A RTColor object containing the color of the point.
	 */
	public final RTColor getColorAtPoint(final Point pt) {
		if (pt.x >= bWidth || pt.x < 0 || pt.y < 0 || pt.y >= bHeight) {
			throw new IllegalArgumentException("Invalid point coordinate");
		}
		if (bColorBitmap != null) {
			return bColorBitmap[pt.y][pt.x];
		} else {
			return null;
		}
	}

	/**
	 * Return the width of the bitmap.
	 * 
	 * @return Width of the bitmap.
	 */
	public final int width() {
		return bWidth;
	}

	/**
	 * Return the height property of the bitmap
	 * 
	 * @return Height of the bitmap.
	 */
	public final int height() {
		return bHeight;
	}

	/**
	 * Read a bitmap from a PPM file. Binary and ACSII versions of PPM are both
	 * supported. This is a kind of initializer method to initialize the bitmap.
	 * This can be combinationally used with the default constructor.
	 * 
	 * @param path
	 *            The path of the PPM file in system.
	 */
	public final void readFromPPM(final String path) {
		if (path == null) {
			throw new IllegalArgumentException("Path is null");
		}
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(path);
		} catch (FileNotFoundException e1) {
			e1.printStackTrace();
		}
		BufferedReader input = new BufferedReader(new InputStreamReader(fis));
		try {
			Boolean isBinaryPPM = null;

			// Read magic number
			String magicNumberStr = getLineFromBinaryPPM(fis, true);
			if (magicNumberStr.startsWith("P3")) {
				isBinaryPPM = false;
			} else if (magicNumberStr.startsWith("P6")) {
				isBinaryPPM = true;
			}

			// Read bitmap size
			String sizeStr = getLineFromBinaryPPM(fis, true);
			String[] sizeArray = sizeStr.split(" ");
			bWidth = new Integer(sizeArray[0]).intValue();
			bHeight = new Integer(sizeArray[1]).intValue();

			// Read bitmap max color component value
			getLineFromBinaryPPM(fis, true);

			// Read bitmap data, binary
			if (isBinaryPPM) {
				bColorBitmap = readBitmapFromBinary(fis);
			} else {
				bColorBitmap = readBitmapFromASCII(input);
			}
			input.close();

		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * This is a private method only used in RTBitmap class itself, it read from
	 * the param input to get Bytes of binary color data and extract the color
	 * data into RTColor object. Then store them into an 2-D array of RTColor,
	 * which will be returned.
	 * 
	 * @param fis
	 *            The bufferedReader initialized while the start of reading PPM.
	 * @return An 2-D array of RTColor which are the data of the bitmap
	 *         extracted from the binary PPM file.
	 * @throws IOException
	 *             Exception
	 */
	private RTColor[][] readBitmapFromBinary(final FileInputStream fis)
			throws IOException {
		RTColor[][] matrix = new RTColor[bHeight][bWidth];

		for (int i = 0; i < bHeight; i++) {
			for (int j = 0; j < bWidth; j++) {
				matrix[i][j] = new RTColor(0.0f, 0.0f, 0.0f);
			}
		}
		byte[] bytes = getRestFromBinaryPPM(fis);

		int bytesIndex = 0;
		for (int xIndex = 0; xIndex < bHeight; xIndex++) {
			for (int yIndex = 0; yIndex < bWidth; yIndex++) {
				int bTRANSFORMER = 0xFF;
				if (bytesIndex + 3 >= bytes.length) {
					break;
				}
				int r = (int) (bytes[bytesIndex++] & bTRANSFORMER);
				int g = (int) (bytes[bytesIndex++] & bTRANSFORMER);
				int b = (int) (bytes[bytesIndex++] & bTRANSFORMER);
				matrix[xIndex][yIndex] = new RTColor(r, g, b);
			}
		}
		return matrix;
	}

	/**
	 * Private method to read all the rest data from the input FileInputStream
	 * 
	 * @param input
	 *            A FileInputStream object representing the XML file.
	 * @return A bytes array contains all bytes of the ppm.
	 * @throws IOException
	 *             Exception
	 */
	private byte[] getRestFromBinaryPPM(final FileInputStream input)
			throws IOException {
		byte[] bytes = new byte[bHeight * bWidth * 3];
		int count = 0;
		while (true) {
			int ch = input.read();
			if (ch != -1) {
				bytes[count] = (byte) ch;
				count++;
			} else {
				break;
			}
		}
		return bytes;
	}

	/**
	 * This is a private method only used in RTBitmap class itself, it read from
	 * the param input to get ASCII color components value and initialize the
	 * color data into RTColor object. Then store them into an 2-D array of
	 * RTColor, which will be returned.
	 * 
	 * @param input
	 *            The bufferedReader initialized while the start of reading PPM.
	 * @return An 2-D array of RTColor which are the data of the bitmap
	 *         extracted from the ASCII PPM file.
	 * @throws IOException
	 *             Exception
	 */
	private RTColor[][] readBitmapFromASCII(final BufferedReader input)
			throws IOException {
		RTColor[][] matrix = new RTColor[bHeight][bWidth];
		for (int i = 0; i < bHeight; i++) {
			for (int j = 0; j < bWidth; j++) {
				matrix[i][j] = new RTColor(0.0f, 0.0f, 0.0f);
			}
		}
		String whole = getRestFromASCPPM(input);

		// Strip out \n\r if they exists
		whole = whole.replace('\n', '\u0000');
		whole = whole.replace('\r', '\u0000');

		String[] colorCmptArray = whole.split(" ");

		int index = 0;
		for (int yIndex = 0; yIndex < bHeight; yIndex++) {
			for (int xIndex = 0; xIndex < bWidth; xIndex++) {
				int r = new Integer(colorCmptArray[index]).intValue();
				int g = new Integer(colorCmptArray[index + 1]).intValue();
				int b = new Integer(colorCmptArray[index + 2]).intValue();
				matrix[yIndex][xIndex] = new RTColor(r, g, b);
				index += 3;
			}
		}
		return matrix;
	}

	/**
	 * This is private method to get the rest data from the parameter
	 * bufferedReader and return the data as a String.
	 * 
	 * @param input
	 *            The bufferedReader, which was initialized while the start of
	 *            reading a PPM file.
	 * @return A String object, which contains all data of the input form the
	 *         current buffer pointer to the end.
	 * @throws IOException
	 *             Exception
	 */
	private String getRestFromASCPPM(final BufferedReader input)
			throws IOException {
		StringBuilder contents = new StringBuilder();
		String line;
		while (true) {
			line = getLineFromASCPPM(input, false);
			if (line != null) {
				contents.append(line);
				if (!input.ready()) {
					break;
				}
			}
		}
		return contents.toString();
	}

	/**
	 * Private method to get a single line from binary PPM
	 * 
	 * @param input
	 *            the file input
	 * @param ignoreComments
	 *            whether ignore the comment or not
	 * @return a line read from bitmap
	 * @throws IOException
	 *             Exception
	 */
	private String getLineFromBinaryPPM(final FileInputStream input,
			final boolean ignoreComments) throws IOException {
		StringBuilder contents = new StringBuilder();
		int ch;
		while ((ch = input.read()) != -1) {
			// Get rid of comments
			if (ch == 35) {
				while ((ch = input.read()) != -1) {
					if (ch == 10)
						break;
				}
				continue;
			}
			// Append chars until new line feed
			if (ch != 10) {
				contents.append((char) ch);
			} else {
				break;
			}
		}
		return contents.toString().equals("") ? null : contents.toString();
	}

	/**
	 * This is private method to get a single line from the parameter
	 * bufferedReader and return the line as a String. If the line contains only
	 * a line separator or it's started with a "#", the line will be skipped
	 * until a valid line is returned.
	 * 
	 * @param input
	 *            The bufferedReader, which was initialized while the start of
	 *            reading a PPM file.
	 * @param ingoreComments
	 *            whether ignore comments or not
	 * @return A String object of a line characters.
	 * @throws IOException
	 *             Exception
	 */
	private String getLineFromASCPPM(final BufferedReader input,
			final boolean ignoreComments) throws IOException {
		StringBuilder contents = new StringBuilder();
		try {
			String line = null;
			while ((line = input.readLine()) != null) {
				if (ignoreComments && line.startsWith("#")) {
					continue;
				}
				contents.append(line);
				break;
			}
		} catch (IOException ex) {
			ex.printStackTrace();
		}
		return contents.toString().equals("") ? null : contents.toString();
	}

	/**
	 * Write the bitmap color data to a PPM(binary format) file.
	 * 
	 * @param path
	 *            the image path
	 */
	public final void writeBitmapToBinaryPPM(final String path) {
		File fl = new File(path);

		FileOutputStream fos;
		try {
			fos = new FileOutputStream(fl);
			fos.write("P6\n".getBytes());
			fos.write(String.valueOf(bWidth).getBytes());
			fos.write(' ');
			fos.write((String.valueOf(bHeight) + "\n").getBytes());
			fos.write((String.valueOf(255) + "\n").getBytes());

			// no more than 70
			byte[] bytes = new byte[bHeight * bWidth * 3];
			int index = 0;
			for (int i = 0; i < bHeight; i++) {
				for (int j = 0; j < bWidth; j++) {
					int bTRANSFORMER = 0xFF;
					bytes[index++] = (byte) (bColorBitmap[i][j].getRed() & bTRANSFORMER);
					bytes[index++] = (byte) (bColorBitmap[i][j].getGreen() & bTRANSFORMER);
					bytes[index++] = (byte) (bColorBitmap[i][j].getBlue() & bTRANSFORMER);
				}
			}

			fos.write(bytes);
			fos.flush();
			fos.close();

		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Write the bitmap color data to a PPM(ASCII format) file.
	 * 
	 * @param path
	 *            the image path
	 */
	public final void writeBitmapToASCIIPPM(final String path) {
		File fl = new File(path);
		FileOutputStream fos;
		try {
			fos = new FileOutputStream(fl);
			BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(fos));
			bw.write("P3\n");
			bw.write(String.valueOf(bWidth));
			bw.write(' ');
			bw.write(String.valueOf(bHeight) + "\n");
			bw.write(String.valueOf(255) + "\n");

			int counter = 0;
			// used to control the
			// maximal character per line is
			// no more than 70
			for (int i = 0; i < bHeight; i++) {
				for (int j = 0; j < bWidth; j++) {
					counter++;

					bw.write(String.valueOf(bColorBitmap[i][j].getRed()));
					bw.write(' ');
					bw.write(String.valueOf(bColorBitmap[i][j].getGreen()));
					bw.write(' ');
					bw.write(String.valueOf(bColorBitmap[i][j].getBlue()));
					bw.write(' ');
					if (counter >= 5) {
						bw.write("\n");
						counter = 0;
					}
				}
			}
			bw.flush();
			bw.close();
			fos.close();

		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
